package pl.un4giv3n.mightvsmagic.utils;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import pl.un4giv3n.mightvsmagic.entity.Action;
import pl.un4giv3n.mightvsmagic.entity.ActionType;
import pl.un4giv3n.mightvsmagic.entity.FightReport;
import pl.un4giv3n.mightvsmagic.entity.Item;
import pl.un4giv3n.mightvsmagic.entity.ItemStub;
import pl.un4giv3n.mightvsmagic.entity.Reward;
import pl.un4giv3n.mightvsmagic.entity.Round;
import pl.un4giv3n.mightvsmagic.entity.User;
import pl.un4giv3n.mightvsmagic.entity.UserAttribute;
import pl.un4giv3n.mightvsmagic.entity.UserStub;
import pl.un4giv3n.mightvsmagic.entity.UserType;

/**
 * @author andrzejewskia
 * Support class for fights
 */
public class FightUtil {

	private static final String RUN_DESC = " ucieka z pola bitwy, gdyż jego energia życiowa spada poniżej 5";
	public static final long SF_TIME = 1000 * 1800; //30 min
	public static final long MIN_DUR_FOR_SAME_PLAYERS = 1000 * 2700;//45min
	public static final String SF_TIME_STR = "30";
	public static final String MIN_DUR_FOR_SAME_PLAYERS_STR = "45";
	
	private FightUtil() {
	}
	
	private enum ExpType {
		FIGHT, MAGIC
	}
	
	/**
	 * @param user
	 * @return true if user can attack other users
	 */
	public static synchronized boolean canAttack(User user) {
		Action currentAction = user.getCurrentAction();
		if (currentAction == null || currentAction.equals(Action.NO_ACTION)
				|| currentAction.getActionType() == ActionType.FIGHT) {
			long lastFightTime = user.getLastFightTime();
			long now = System.currentTimeMillis();
			long diff = now - lastFightTime;
			return user.isPremium() ? diff > ActionType.FIGHT
					.getPremiumDuration() : diff > ActionType.FIGHT
					.getDuration();
		} else {
			return false;
		}

	}

	/**
	 * @param user
	 * @return true if the user can be attacked
	 */
	public static synchronized boolean canDefend(User user) {
		long lastFightTime = user.getLastFightTime();
		long now = System.currentTimeMillis();
		long diff = now - lastFightTime;
		return diff > SF_TIME;
	}

	/**
	 * @param attacker
	 * @param defender
	 * @return fight result between the users
	 */
	public static synchronized FightReport fight(User attacker, User defender) {
		int attackerRounds = calculateRounds(attacker);
		int attackerRoundsDone = 0;
		int defenderRounds = calculateRounds(defender);
		int defenderRoundsDone = 0;
		long attackerSpeed = UserUtil.getUserSkillLevel(attacker, UserAttribute.SPEED);
		long defenderSpeed = UserUtil.getUserSkillLevel(defender, UserAttribute.SPEED);
		long attackerSpeedrate = (long)((1/Double.valueOf(attackerSpeed))*1000);
		long defenderSpeedrate = (long)((1/Double.valueOf(defenderSpeed))*1000);
		FightReport fight = new FightReport(createStub(attacker), createStub(defender));
		setAdditionalAttributes(attacker, defender, fight);
		boolean endFight = false;
		for(int i=1;;i++){
			if((i%attackerSpeedrate == 0) && attackerRoundsDone < attackerRounds){
				endFight = addRound(fight, attack(attacker, defender,
						attackerRoundsDone++));
			}
			if (endFight) {
				break;
			}
			if((i%defenderSpeedrate == 0) && defenderRoundsDone < defenderRounds){
				endFight = addRound(fight, attack(defender, attacker,
						defenderRoundsDone++));
			}
			if (endFight) {
				break;
			}
			//users used all their rounds
			if(attackerRoundsDone >= attackerRounds &&  defenderRoundsDone >= defenderRounds){
				break;
			}
			
		}
		User winner = getWinner(fight, attacker, defender);
		fight.setWinner(winner != null ? winner.getLogin() : "");
		fight.setWinnerId(winner != null ? winner.getId() : null);
		fight.setReward(getReward(fight, attacker, defender));
		fight.setLooserBonusFightExp(getLooserBonusExp(fight, attacker, defender, ExpType.FIGHT));
		fight.setLooserBonusMagicExp(getLooserBonusExp(fight, attacker, defender, ExpType.MAGIC));
		return fight;
	}

	private static int getLooserBonusExp(FightReport fight, User attacker,
			User defender, ExpType expType) {
		User winner = null;
		User looser = null;
		int bonuxExp = 0;
		if (fight.getWinner().equals(attacker.getLogin())) {
			winner = attacker;
			looser = defender;
		} else {
			winner = defender;
			looser = attacker;
		}
		if(expType == ExpType.FIGHT){
			if(looser.getUserType() == UserType.KNIGHT){
				if (winner.getFightExp() <= looser.getFightExp()) {
					bonuxExp = 1;
				}
			}
		}else{
			if(looser.getUserType() == UserType.MAGE){
				if (winner.getMagicExp() <= looser.getMagicExp()) {
					bonuxExp = 1;
				}
			}
		}
		return bonuxExp;
	}

	private static void setAdditionalAttributes(User attacker, User defender,
			FightReport fight) {
		fight.setAttackerDef(UserUtil.getTotalMinDefense(attacker) + "-" + UserUtil.getTotalMaxDefense(attacker));
		fight.setAttackerDmg(UserUtil.getTotalMinDamage(attacker) + "-" + UserUtil.getTotalMaxDamage(attacker));
		fight.setDefenderDef(UserUtil.getTotalMinDefense(defender) + "-" + UserUtil.getTotalMaxDefense(defender));
		fight.setDefenderDmg(UserUtil.getTotalMinDamage(defender) + "-" + UserUtil.getTotalMaxDamage(defender));
		fight.setDefenderIceDmg(UserUtil.getIceDamage(defender));
		fight.setDefenderFireDmg(UserUtil.getFireDamage(defender));
		fight.setDefenderEarthDmg(UserUtil.getEarthDamage(defender));
		fight.setDefenderWindDmg(UserUtil.getWindDamage(defender));
		fight.setDefenderIceDef(UserUtil.getIceDefence(defender));
		fight.setDefenderFireDef(UserUtil.getFireDefence(defender));
		fight.setDefenderEarthDef(UserUtil.getEarthDefence(defender));
		fight.setDefenderWindDef(UserUtil.getWindDefence(defender));
		fight.setAttackerIceDmg(UserUtil.getIceDamage(attacker));
		fight.setAttackerFireDmg(UserUtil.getFireDamage(attacker));
		fight.setAttackerEarthDmg(UserUtil.getEarthDamage(attacker));
		fight.setAttackerWindDmg(UserUtil.getWindDamage(attacker));
		fight.setAttackerIceDef(UserUtil.getIceDefence(attacker));
		fight.setAttackerFireDef(UserUtil.getFireDefence(attacker));
		fight.setAttackerEarthDef(UserUtil.getEarthDefence(attacker));
		fight.setAttackerWindDef(UserUtil.getWindDefence(attacker));
	}

	private static UserStub createStub(User user) {
		List<ItemStub> items = new ArrayList<ItemStub>();
		for (Item item : user.getItems()) {
			if(item.isInUse()){
				items.add(new ItemStub(item.getName(), item.getImgUrl()));
			}
		}
		UserStub stub = new UserStub(user.getLogin(), user.getId(),
				UserUtil.getUserSkillLevel(user, UserAttribute.STRENGHT),
				UserUtil.getUserSkillLevel(user, UserAttribute.ATTACK),
				UserUtil.getUserSkillLevel(user, UserAttribute.PARRY),
				UserUtil.getUserSkillLevel(user, UserAttribute.LIFE),
				UserUtil.getUserSkillLevel(user, UserAttribute.STAMINA),
				UserUtil.getUserSkillLevel(user, UserAttribute.MANA),
				UserUtil.getUserSkillLevel(user, UserAttribute.SPEED), items, 
				user.getMagicExp(), user.getFightExp());
		return stub;
	}

	private static User getWinner(FightReport fight, User attacker, User defender) {
		double attackerDmg = getAttackerDamage(fight);
		double defenderDmg = getDefenderDamage(fight);
		//check if fight was ended by zero-ing opponent
		if(attacker.getHp() < 5){
			return defender;
		}
		if(defender.getHp() < 5){
			return attacker;
		}
		//if not, winner is the one who made more damage
		return attackerDmg > defenderDmg ? attacker : 
			attackerDmg < defenderDmg ? defender : null;
	}

	/**
	 * @param fight - fight result to calculate damage
	 * @return damage done by attacker in the fight
	 */
	public static double getAttackerDamage(FightReport fight) {
		double attackerDmg = 0;
		for (Round round : fight.getRounds()) {
			String attacker = round.getAttacker();
			if (fight.getAttackerLogin().equals(attacker)) {
				attackerDmg += round.getDamage();
			}
		}
		return attackerDmg;
	}

	/**
	 * @param fight - fight result to calculate damage
	 * @return damage done by defender in the fight
	 */
	public static double getDefenderDamage(FightReport fight) {
		double defenderDmg = 0;
		for (Round round : fight.getRounds()) {
			String attacker = round.getAttacker();
			if (!fight.getAttackerLogin().equals(attacker)) {
				defenderDmg += round.getDamage();
			}
		}
		return defenderDmg;
	}

	private static Reward getReward(FightReport fight, User attacker,
			User defender) {
		String winnerLogin = fight.getWinner();
		if(winnerLogin.equals("")){
			return Reward.NO_REWARD;
		}
		Reward reward = new Reward();
		User winner = null;
		User looser = null;
		if (winnerLogin.equals(attacker.getLogin())) {
			winner = attacker;
			looser = defender;
		} else {
			winner = defender;
			looser = attacker;
		}
		if(winner.getUserType() == UserType.KNIGHT){
			if (winner.getFightExp() < looser.getFightExp()) {
				reward.setRewardedFightExp(3);
			}
			if (winner.getFightExp() == looser.getFightExp()) {
				reward.setRewardedFightExp(1);
			}
			if (winner.getMagicExp() < looser.getMagicExp()) {
				reward.setRewardedMagicExp(1);
			}
		}else if(winner.getUserType() == UserType.MAGE){
			if (winner.getMagicExp() < looser.getMagicExp()) {
				reward.setRewardedMagicExp(3);
			}
			if (winner.getFightExp() < looser.getFightExp()) {
				reward.setRewardedFightExp(1);
			}
			if (winner.getMagicExp() == looser.getMagicExp()) {
				reward.setRewardedFightExp(1);
			}
		}
		reward.setRewardedGold(Math.round(0.1 * looser.getGold()));
		return reward;
	}

	private static boolean addRound(FightReport fight, Round round) {
		List<Round> rounds = fight.getRounds();
		rounds.add(round);
		if (round.getDescription().indexOf(RUN_DESC) != -1) {
			return true;
		}
		return false;
	}

	private static Round attack(User attacker, User defender, int round) {
		// atakujący nie ma siły już atakować
		if (attacker.getHp() < 0) {
			return new Round(attacker.getLogin()+ RUN_DESC, 0, attacker);
		}
		if (defender.getHp() < 0) {
			return new Round(defender.getLogin()+ RUN_DESC, 0, defender);
		}
		if (getMagicRounds(attacker) > round) {
			double damage = getDamage(attacker, defender);
			String desc = damage > 0 ? " rzuca czar i zadaje "
					+ FormatterUtil.formatDamage(damage) + " obrażeń"
					: " nie trafia";
			defender.setHp(defender.getHp() - damage);
			return new Round(attacker.getLogin() + " " + desc, damage, attacker);
		} else {
			double damage = getDamage(attacker, defender);
			String desc = damage > 0 ? " atakuje i zadaje "
					+ FormatterUtil.formatDamage(damage) + " obrażeń"
					: " nie trafia";
			defender.setHp(defender.getHp() - damage);
			return new Round(attacker.getLogin() + " " + desc, damage, attacker);
		}
	}

	private static double getDamage(User attacker, User defender) {
		return isHit(attacker, defender) ? calculateDamage(attacker, defender)
				: 0;
	}

	private static boolean isHit(User attacker, User defender) {
		double attack = UserUtil.getUserSkillLevel(attacker, UserAttribute.ATTACK);
		double defense = UserUtil.getUserSkillLevel(defender, UserAttribute.PARRY);
		double hitPossibility = Math.random();
		double factor = (attack*attack)/((attack*attack)+(defense*defense));
		return factor > hitPossibility;
	}

	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
		double minDamage = 18;
		double maxDamage = 21;
		double dmg = minDamage + (maxDamage-minDamage) * Math.random();
		double minDefence = 14.5;
		double maxDefence= 20;
		double def = minDefence + (maxDefence-minDefence) * Math.random();
		System.out.println(dmg > def ? dmg - def : 0);
		}
	}
	
	
	private static double calculateDamage(User attacker, User defender) {
		//calculate damage, first get base damage
		double minDamage = UserUtil.getBaseMinDamage(attacker);
		double maxDamage = UserUtil.getBaseMaxDamage(attacker);
		//add possible elemental damage
		if(UserUtil.getIceDamage(attacker) > UserUtil.getIceDefence(defender)){
			maxDamage += UserUtil.getIceDamage(attacker) - UserUtil.getIceDefence(defender);
			minDamage += UserUtil.getIceDamage(attacker) - UserUtil.getIceDefence(defender);
		}
		if(UserUtil.getFireDamage(attacker) > UserUtil.getFireDefence(defender)){
			maxDamage += UserUtil.getFireDamage(attacker) - UserUtil.getFireDefence(defender);
			minDamage += UserUtil.getFireDamage(attacker) - UserUtil.getFireDefence(defender);
		}
		if(UserUtil.getEarthDamage(attacker) > UserUtil.getEarthDefence(defender)){
			maxDamage += UserUtil.getEarthDamage(attacker) - UserUtil.getEarthDefence(defender);
			minDamage += UserUtil.getEarthDamage(attacker) - UserUtil.getEarthDefence(defender);
		}
		if(UserUtil.getWindDamage(attacker) > UserUtil.getWindDefence(defender)){
			maxDamage += UserUtil.getWindDamage(attacker) - UserUtil.getWindDefence(defender);
			minDamage += UserUtil.getWindDamage(attacker) - UserUtil.getWindDefence(defender);
		}
		//add damage from items
		maxDamage += ItemUtil.getMaxItemDamage(attacker);
		minDamage += ItemUtil.getMinItemDamage(attacker);
		//factorize damage basing on skills
		maxDamage = UserUtil.factorizeDamage(maxDamage, attacker);
		minDamage = UserUtil.factorizeDamage(minDamage, attacker);
		//final damage made
		double dmg = minDamage + (maxDamage-minDamage) * Math.random();
		//calculate defence, first get base defence
		double minDefence = UserUtil.getBaseMinDefence(defender);
		double maxDefence= UserUtil.getBaseMaxDefence(defender);
		//add item defense, we dont need to add elemental defence, as it substracted from elemental damage
		minDefence += ItemUtil.getMinItemDefence(defender);
		maxDefence += ItemUtil.getMaxItemDefence(defender);
		//factorize defence basing on skills
		minDefence = UserUtil.factorizeDefence(minDefence, defender);
		maxDefence = UserUtil.factorizeDefence(maxDefence, defender);
		//calculate final defence
		double def = minDefence + (maxDefence-minDefence) * Math.random();
		return dmg > def ? dmg - def : 0;
	}

	private static int getMagicRounds(User user) {
		return Math.round(UserUtil.getUserSkillLevel(user, UserAttribute.MANA) / 5);
	}

	private static int getMightRounds(User user) {
		return Math.round(UserUtil.getUserSkillLevel(user, UserAttribute.STAMINA) / 5);
	}

	private static int calculateRounds(User user) {
		return getMagicRounds(user) + getMightRounds(user);

	}

	@SuppressWarnings("unchecked")
	public static boolean canFightAgain(User attacker, User defender,
			EntityManager entityManager) {
		Query query = entityManager.createNamedQuery("find.fights.between.users");
		query.setParameter("defId", defender.getId());
		query.setParameter("attId", attacker.getId());
		List resultList = query.getResultList();
		if(resultList == null || resultList.isEmpty()){
			return true;
		}
		FightReport report = (FightReport) resultList.get(0);
		return System.currentTimeMillis() - report.getFightTime() > MIN_DUR_FOR_SAME_PLAYERS;
	}

}
